package socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 聊天室的服务端
 * @author Xiloer
 *
 */
public class Server {
	/*
	 * 服务端使用的是ServerSocket
	 * 它有两个作用:
	 * 1:向系统申请端口。
	 * 2:接收请求该端口的所有客户端的连接。
	 */
	private ServerSocket server;
	/*
	 * 内部类可以访问外部类的属性，因此所有ClientHandler都可以访问到外部类
	 * Server的属性，所以在这里定义一个数组保存所有ClientHandler对应客户端的
	 * 输出流就可以做到共享了。以便每个客户端发消息过来对应的ClientHandler都可
	 * 以通过遍历这个数组拿到所有对应客户端的输出流广播消息。
	 */
	private List<PrintWriter> allOut 
			= Collections.synchronizedList(new ArrayList<>());
	/**
	 * 服务端构造方法，用来初始化服务端
	 */
	public Server() {
		try {
			/*
			 * 实例化的同时向系统申请服务端口，客户端Socket就是通过这个
			 * 端口与服务端建立连接的。如果该端口被系统其他程序占用了则
			 * 会抛出异常:
			 * java.net.BindException:address already in use: JVM_bind
			 * 
			 * 遇到该错误时解决办法:
			 * 1:首先检查是否自己开启过两次服务端，因为第一次启动已经占用了
			 *   该端口，如果再启动一遍时还申请该端口就会提示被占用了。
			 * 2:如果没有启动过两次，那说明系统其他程序占用该端口了，需要更
			 *   换一个可用的。  
			 */
			System.out.println("正在启动服务端...");
			server = new ServerSocket(8088);
			System.out.println("服务端启动完毕!");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * 服务端开始工作的方法
	 */
	public void start() {
		try {
			/*
			 * ServerSocket提供的方法:
			 * Socket accept()
			 * 该方法是一个阻塞方法，调用该方法后程序"卡住",此时开始等待
			 * 客户端的连接，一旦客户端实例化Socket并连接服务端这边申请的
			 * 端口(8088)时，accept方法会立即返回一个Socket实例，此时等于
			 * 和客户端建立了连接。服务端通过返回的这个Socket就可以与客户端
			 * 进行交互了。
			 * 多次调用accept方法可以接收多个客户端的连接。
			 */
			while(true) {
				System.out.println("等待客户端连接...");
				Socket socket = server.accept();
				System.out.println("一个客户端连接了!");
				//启动一个线程处理该客户端交互
				ClientHandler handler = new ClientHandler(socket);
				Thread t = new Thread(handler);
				t.start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		Server server = new Server();
		server.start();
	}
	/**
	 * 该线程任务用来与指定客户端交互
	 * @author Xiloer
	 *
	 */
	private class ClientHandler implements Runnable{
		private Socket socket;
		private String host;//记录客户端的IP地址信息
		private String message;
		
		public ClientHandler(Socket socket) {
			this.socket = socket;
			//通过socket获取远端计算机地址信息(对于服务端这边而言远端就是客户端)
			host = socket.getInetAddress().getHostAddress();
		}
		
		public void run() {
			PrintWriter pw = null;
			try {
				/*
				 * Socket提供的方法:
				 * InputStream getInputStream();
				 * 通过该方法获取的输入流读取的字节是远端计算机发送过来的字节
				 */
				InputStream in = socket.getInputStream();
				InputStreamReader isr = new InputStreamReader(in,"UTF-8");
				BufferedReader br = new BufferedReader(isr);
				
				OutputStream out = socket.getOutputStream();
				OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
				BufferedWriter bw = new BufferedWriter(osw);
				pw = new PrintWriter(bw,true);
				/*
				 * 将输出流存入共享数组allOut中
				 */
//				synchronized (allOut) {
//					//1将allOut数组扩容
//					allOut = Arrays.copyOf(allOut, allOut.length+1);				
//					//2将输出流存入数组最后一个位置
//					allOut[allOut.length-1] = pw;
//				}
				allOut.add(pw);	
				System.out.println(host+"上线了，当前在线人数:"+allOut.size());
				
				while((message = br.readLine())!=null) {
					System.out.println(host + "说:" + message);
//					synchronized (allOut) {
//						//回复所有客户端
//						for(int i=0;i<allOut.length;i++) {
//							allOut[i].println(host + "说:" + message);
//						}
//					}
					allOut.forEach(
						(p)->p.println(host + "说:" + message)
					);
				}
			} catch (Exception e) {
				
			} finally {
				//处理客户端断开连接后的操作
				
//				synchronized (allOut) {
//					//将对应当前客户端的输出流从数组allOut中删除
//					for(int i=0;i<allOut.length;i++) {
//						if(allOut[i]==pw) {
//							allOut[i] = allOut[allOut.length-1];
//							allOut = Arrays.copyOf(allOut, allOut.length-1);
//							break;
//						}
//					}
//				}
				allOut.remove(pw);
				System.out.println(host+"下线了，当前在线人数:"+allOut.size());
				
				try {
					/*
					 * socket的close方法调用后会与远端计算机断开连接，此时
					 * 通过socket获取的输入流与输出流也被close了。
					 */
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}		
	}
	
}


